package com.tiny.tinyweb.core.test;

import javax.naming.Name;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ldap.core.LdapAttributes;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.ldif.parser.LdifParser;
import org.springframework.ldap.ldif.parser.Parser;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.ldap.test.LdapTestUtils;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.tiny.tinyweb.core.annotation.LdapTestData;
import com.tiny.tinyweb.core.log.LogInfo;

/**
 * 实现自 {@link org.springframework.test.context.transaction.AbstractTestExecutionListener}
 * 的监听器类。该类将根据 {@link LdapTestData} 注解的配置信息来准备测试用的条目数据。包含测试数据的 ldif
 * 文件中不能出现根条目，因为目录服务器工厂对象中已经配置了根条目。测试的执行顺序如下:
 * <ul>
 * <li>{@link AbstractTestExecutionListener#beforeTestClass}</li>
 * <li>{@link org.junit.BeforeClass} 注解的方法</li>
 * <li>{@link AbstractTestExecutionListener#prepareTestInstance}</li>
 * <li>{@link AbstractTestExecutionListener#beforeTestMethod}</li>
 * <li>{@link org.junit.Before} 注解的方法</li>
 * <li>{@link AbstractTestExecutionListener#afterTestMethod}</li>
 * <li>{@link org.junit.After} 注解的方法</li>
 * <li>{@link AbstractTestExecutionListener#afterTestClass}</li>
 * <li>{@link org.junit.AfterClass} 注解的方法</li>
 * </ul>
 * @see {@link org.springframework.test.context.AbstractTestExecutionListener}
 * @author wxiaobin
 */
public class UnitTestLdapExecutionListener extends AbstractTestExecutionListener {
    /**
     * 用 {@link UnitTestLdapExecutionListener} 实例化的 {@link org.slf4j.Logger} 对象。
     */
    protected Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 用来保存日志信息的对象。
     */
    protected LogInfo logInfo = new LogInfo();

    /**
     * 由数据源工厂 {@link org.springframework.ldap.test.TestContextSourceFactoryBean}
     * 对象创建的访问 ApacheDS 目录服务器的数据源对象。
     */
    private LdapContextSource contextSource;

    /**
     * LDAP 操作模板对象。
     */
    private LdapTemplate ldapTemplate;

    /**
     * 从测试环境获得访问 ApacheDS 目录服务器的数据源，并加载数据文件中的条目信息。
     * @param testContext Spring测试框架管理的测试上下文
     * @see {@link org.springframework.test.context.AbstractTestExecutionListener#beforeTestClass(TestContext)}
     */
    @Override
    public void beforeTestClass(TestContext testContext) throws Exception {
        // 从测试环境获得访问 ApacheDS 目录服务器的数据源和 LDAP 操作模板对象
        this.contextSource = (LdapContextSource) testContext
                .getApplicationContext().getBean("contextSource");
        this.ldapTemplate = (LdapTemplate) testContext
                .getApplicationContext().getBean("ldapTemplate");

        // 将数据源信息输出到调试日志
        this.logInfo.setOperation("beforeTestClass");
        this.logInfo.setMessage(
                "ApacheDS[URL=[%1$s], Base=[%2$s], User=[%3$s], Password=[%4$s]]",
                ToStringBuilder.reflectionToString(
                    this.contextSource.getUrls(), ToStringStyle.SIMPLE_STYLE),
                this.contextSource.getBaseLdapPathAsString(),
                this.contextSource.getAuthenticationSource().getPrincipal(),
                this.contextSource.getAuthenticationSource().getCredentials());
        this.logger.info(logInfo.toString());

        // 检查 LdapTestData 注解，载入测试数据
        LdapTestData testData = testContext.getTestClass()
                .getAnnotation(LdapTestData.class);
        if (testData != null) {
            // 将 ldif 文件信息输出到调试日志
            this.logInfo.setMessage("LDIF file is [%1$s].", testData.ldifFile());
            this.logger.debug(logInfo.toString());
            // 载入测试数据
            LdapTestUtils.loadLdif(this.contextSource,
                    new ClassPathResource(testData.ldifFile()));
            // 将 ldif 文件加载成功的信息输出到消息日志
            this.logInfo.setMessage("Loaded LDIF file[%1$s] successfully.",
                    testData.ldifFile());
            this.logger.info(logInfo.toString());
        }
    }

    /**
     * 获得 {@link LdapTestData} 注解指定的 ldif 文件和文件中条目所在的根节点名称，准备测试数据。
     * @param testContext Spring测试框架管理的测试上下文
     * @see {@link org.springframework.test.context.AbstractTestExecutionListener#beforeTestMethod(TestContext)}
     */
    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        // 检查 LdapTestData 注解，载入测试数据
        LdapTestData testData = testContext.getTestMethod()
                .getAnnotation(LdapTestData.class);
        if (testData != null) {
            // 获得 LdapTestData 注解指定的 ldif 文件和文件中条目所在的根节点名称
            Assert.hasLength(testData.ldifFile(),
                    "the ldif file of @LdapTestData must not be null");
            Assert.hasLength(testData.rootNode(),
                    "the root node name of @LdapTestData must not be null");
            // 将 ldif 文件信息输出到调试日志
            this.logInfo.setOperation("beforeTestMethod");
            this.logInfo.setMessage("LDIF file is [%1$s], root node is [%2$s].",
                    testData.ldifFile(), testData.rootNode());
            this.logger.debug(logInfo.toString());
            // 载入测试数据
            LdapTestUtils.cleanAndSetup(this.contextSource,
                    LdapUtils.newLdapName(testData.rootNode()),
                    new ClassPathResource(testData.ldifFile()));
            // 将 ldif 文件加载成功的信息输出到消息日志
            this.logInfo.setMessage("Loaded LDIF file[%1$s] to node[%2$s] successfully.",
                    testData.ldifFile(), testData.rootNode());
            this.logger.info(logInfo.toString());
        }
    }

    /**
     * 测试执行后销毁从 {@link LdapTestData} 注解加载的数据。
     * @param testContext Spring测试框架管理的测试上下文
     * @see {@link org.springframework.test.context.AbstractTestExecutionListener#afterTestMethod(TestContext)}
     */
    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        // 检查 LdapTestData 注解，销毁测试数据
        LdapTestData testData = testContext.getTestMethod()
                .getAnnotation(LdapTestData.class);
        this.tearDown(testData, "afterTestMethod");
    }

    /**
     * 测试结束后销毁从 {@link LdapTestData} 注解加载的数据。
     * @param testContext Spring测试框架管理的测试上下文
     * @see {@link org.springframework.test.context.AbstractTestExecutionListener#afterTestClass(TestContext)}
     */
    @Override
    public void afterTestClass(TestContext testContext) throws Exception {
        // 检查 LdapTestData 注解，销毁测试数据
        LdapTestData testData = testContext.getTestClass()
                .getAnnotation(LdapTestData.class);
        this.tearDown(testData, "afterTestClass");
    }

    /**
     * 根据 ldif 文件删除目录服务器中相应的条目信息。
     * @param testData ldif 文件
     * @param method 执行数据销毁处理的目标方法名
     * @throws Exception 数据销毁异常
     */
    private void tearDown(LdapTestData testData, String method) throws Exception {
        if (testData != null && StringUtils.hasLength(testData.ldifFile())) {
            // 将 ldif 文件信息输出到调试日志
            this.logInfo.setOperation(method);
            this.logInfo.setMessage("LDIF file is [%1$s].", testData.ldifFile());
            this.logger.debug(logInfo.toString());
            // 解析 LdapTestData 注解指定的 ldif 文件
            Parser parser = new LdifParser(
                    new ClassPathResource(testData.ldifFile()));
            parser.open();
            // 将 ldif 文件解析成功的信息输出到调试日志
            this.logInfo.setMessage("Parsed LDIF file[%1$s] successfully.",
                    testData.ldifFile());
            this.logger.debug(logInfo.toString());
            // 循环检查 ldif 文件中指定的条目是否存在，存在则删除
            while (parser.hasMoreRecords()) {
                Name dn = this.buildDN(parser.getRecord());
                try {
                    if (this.ldapTemplate.lookup(dn) != null) {
                        LdapTestUtils.clearSubContexts(this.contextSource, dn);
                        // 删除成功时输出调试日志
                        this.logInfo.setMessage(
                                "Object[%1$s] is cleared(contains its children).",
                                dn.toString());
                        this.logger.debug(logInfo.toString());
                    }
                } catch (Exception e) {
                    // 删除错误时输出调试日志
                    this.logInfo.setMessage("No such object[%1$s] can be cleared.",
                            dn.toString());
                    this.logger.debug(logInfo.toString());
                }
            }
            // 将销毁测试数据成功的信息输出到消息日志
            this.logInfo.setMessage(
                    "Cleared sub contexts by LDIF file[%1$s] successfully.",
                    testData.ldifFile());
            this.logger.info(logInfo.toString());
        }
    }

    /**
     * 从指定的条目对象中剥离根DN信息，返回仅属于该条目的DN对象。
     * @param record 指定的条目对象
     * @return 仅属于该条目的DN对象
     */
    private Name buildDN(Attributes record) {
        LdapName dn = ((LdapAttributes) record).getName();
        LdapUtils.removeFirst(dn, this.contextSource.getBaseLdapName());
        return dn;
    }
}
